7.1 选择修订版本
7.2 交互式暂存
7.3 贮藏与清理
7.4 签署工作
7.5 搜索
7.6 重写历史
7.7 重置揭密
7.8 高级合并
7.9 Rerere
7.10 使用 Git 调试
7.11 子模块
7.12 打包
7.13 替换
7.14 凭证存储
7.15 总结
2020.11,14 星期六 18:12
7.1 选择修订版本
Git 能够以多种方式来指定单个提交、一组提交、或者一定范围内的提交。
单个修订版本
你可以通过任意一个提交的 40 个字符的完整 SHA-1 散列值来指定它, 不过
简短的 SHA-1
git log; git show xx23
如果你在 git log 后加上 --abbrev-commit 参数,输出结果里就会显示简短且唯一的值;git log --abbrev-commit --pretty=oneline
分支引用
引用特定提交的一种直接方法是,若它是一个分支的顶端的提交, 那么可以在任何需要引用该提交的 Git 命令中直接使用该分支的名称。
git rev-parse topic1
引用日志
每当你的 HEAD 所指向的位置发生了变化,Git 就会将这个信息存储到引用日志这个历史记录里。
这个方法只对还在你引用日志里的数据有用,所以不能用来查好几个月之前的提交。1
2
3
4git reflog
git show HEAD@{5}
git show master@{yesterday}
git log -g master # 来查看类似于 git log 输出格式的引用日志信息
祖先引用
如果你在引用的尾部加上一个 ^(脱字符), Git 会将其解析为该引用的上一个提交。你也可以在 ^ 后面添加一个数字来指明想要 哪一个 父提交
这个语法只适用于合并的提交,因为合并提交会有多个父提交。 合并提交的第一父提交是你合并时所在分支(通常为 master),而第二父提交是你所合并的分支(例如 topic)
另一种指明祖先提交的方法是 ~(波浪号)。
而区别在于你在后面加数字的时候。 HEAD~2 代表“第一父提交的第一父提交”,也就是“祖父提交”——Git 会根据你指定的次数获取对应的第一父提交。
提交区间
双点
查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。(在 experiment 分支中而不在 master 分支中的提交)git log master..experiment
另一个常用的场景是查看你即将推送到远端的内容:git log origin/master..HEAD
多点
查看哪些提交是被包含在某些分支中的一个,但是不在你当前的分支上。 Git 允许你在任意引用前加上 ^ 字符或者 –not 来指明你不希望提交被包含其中的分支。查看所有被 refA 或 refB 包含的但是不被 refC 包含的提交
1 | $ git log refA refB ^refC |
三点
这个语法可以选择出被两个引用 之一 包含但又不被两者同时包含的提交。
如果你想看 master 或者 experiment 中包含的但不是两者共有的提交git log master...experimentgit log --left-right master...experiment
7.2 交互式暂存
将文件的特定部分组合成提交。运行 git add 时使用 -i 或者 –interactive 选项 —基本上与 git status 是相同的信息,但是更简明扼要一些。 它将暂存的修改列在左侧,未暂存的修改列在右侧。
在这块区域后是“Commands”命令区域。 在这里你可以做一些工作,包括暂存文件、取消暂存文件、暂存文件的一部分、添加未被追踪的文件、显示暂存内容的区别。
$_PS: 削微操作繁琐,实际没有过
### 暂存与取消暂存文件
### 暂存补丁
Git 也可以暂存文件的特定部分。
可以在命令行中使用
git add -p 或 git add --patch 来启动同样的脚本。更进一步地,可以使用
git reset --patch 命令的补丁模式来部分重置文件,通过
git checkout --patch 命令来部分检出文件与
git stash save --patch 命令来部分暂存文件。$_PS: 补丁不接触
## 7.3 贮藏与清理
贮藏(stash)会处理工作目录的脏的状态——即跟踪文件的修改与暂存的改动——然后将未完成的修改保存到一个栈上, 而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。
> 迁移到 git stash push. 弃用了 git stash save 命令
### 贮藏工作
1 | $ git reset HEAD CONTRIBUTING.md |
git reset 确实是个危险的命令,如果加上了 –hard 选项则更是如此。 然而在上述场景中,工作目录中的文件尚未修改,因此相对安全一些。
请务必记得git checkout -- <file>是一个危险的命令。 你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。 除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。
三棵树
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。
Index 索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区”,这就是当你运行 git commit 时 Git 看起来的样子。
Working Directory 工作目录(通常也叫 工作区).沙盒
重置的作用
让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。
第 1 步:移动 HEAD
无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset –soft,它将仅仅停在那儿。
现在你可以更新索引并再次运行 git commit 来完成 git commit –amend 所要做的事情了(见 修改最后一次提交)。
第 2 步:更新索引(–mixed)
如果指定 –mixed 选项,reset 将会在这时停止。
它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。
第 3 步:更新工作目录(–hard)
reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 –hard 选项,它将会继续这一步。
回顾
reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:
移动 HEAD 分支的指向 (若指定了 –soft,则到此停止)
使索引看起来像 HEAD (若未指定 –hard,则到此停止)
使工作目录看起来像索引
通过路径来重置
若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。
这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新,所以重置会继续进行第 2、3 步。
现在,假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 –soft 或 –hard),它会:
移动 HEAD 分支的指向 (已跳过)
让索引看起来像 HEAD (到此处停止)
所以它本质上只是将 file.txt 从 HEAD 复制到索引中。
它还有 取消暂存文件 的实际效果。git reset eb43bf file.txt
还有一点同 git add 一样,就是 reset 命令也可以接受一个 –patch 选项来一块一块地取消暂存的内容。
压缩
可以运行 git reset --soft HEAD~2 来将 HEAD 分支移动到一个旧一点的提交上(即你想要保留的最近的提交)
然后只需再次运行 git commit:
检出
和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。
不带路径
运行 git checkout [branch] 与运行git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],
首先不同于 reset –hard,checkout 对工作目录是安全的,
其实它还更聪明一些。
第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。
带路径
运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。
总结
下面的速查表列出了命令对树的影响。 “HEAD” 一列中的 “REF” 表示该命令移动了 HEAD 指向的分支引用,而 “HEAD” 则表示只移动了 HEAD 自身。
特别注意 WD Safe? 一列——如果它标记为 NO,那么运行该命令之前请考虑一下。
Commit Level
– | HEAD | Index | Workdir | WD Safe?
– | – | – | – | –reset --soft [commit] | REF | NO | NO | YESreset [commit] | REF | YES | NO | YESreset --hard [commit] | REF | YES | YES | NOcheckout <commit> | HEAD| YES| YES| YES
File Level
– | HEAD | Index | Workdir | WD Safe?
– | – | – | – | –reset [commit] <paths> | NO | YES | NO | YES
checkout [commit] <paths>| NO| YES| YES| NO
7.8 高级合并
合并冲突
现在我们尝试合并入我们的 whitespace 分支,因为修改了空白字符,所以合并会出现冲突。
中断一次合并
git merge --abort 选项会尝试恢复到你运行合并前的状态。
但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好。
如果出于某些原因你想要重来一次,也可以运行 git reset –hard HEAD 回到上一次提交的状态。
忽略空白
-Xignore-all-space 或 -Xignore-space-change 选项。 第一个选项在比较行时 完全忽略 空白修改,第二个选项将一个空白符与多个连续的空白字符视作等价的。
git merge -Xignore-space-change whitespace#### 手动文件再合并
然后我们想要我的版本的文件,他们的版本的文件(从我们将要合并入的分支)和共同的版本的文件(从分支叉开时的位置)的拷贝。
然后我们想要修复任何一边的文件,并且为这个单独的文件重试一次合并。
我们可以手工修复它们来修复空白问题,然后使用鲜为人知的 git merge-file 命令来重新合并那个文件。1
2
3
4
5
6
7
8
9# 通过 git show 命令与一个特别的语法,你可以将冲突文件的这些版本释放出一份拷贝。
$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb
# 如果你想要更专业一点,也可以使用 `ls-files -u` 底层命令来得到这些文件的 Git blob 对象的实际 SHA-1 值。
# :1:hello.rb 只是查找那个 blob 对象 SHA-1 值的简写。
$ git merge-file -p \
hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
实际上,这比使用 ignore-space-change 选项要更好,因为。。。
如果你想要在最终提交前看一下我们这边与另一边之间实际的修改, 你可以使用 git diff 来比较将要提交作为合并结果的工作目录与其中任意一个阶段的文件差异。 让我们看看它们。1
2
3
4
5
6
7
8
9
10
11
12
13# 看看合并引入了什么,可以运行 git diff --ours
$ git diff --ours
# 如果我们想要查看合并的结果与他们那边有什么不同,可以运行 git diff --theirs。
# 在本例及后续的例子中,我们会使用 -b 来去除空白
$ git diff --theirs -b
# 最终,你可以通过 git diff --base 来查看文件在两边是如何改动的。
$ git diff --base -b
# 在这时我们可以使用 git clean 命令来清理我们为手动合并而创建但不再有用的额外文件。
$ git clean -f
$_PS: 有编辑器插件。手动文件和检出冲突,用少/无。
检出冲突
一个很有用的工具是带 –conflict 选项的 git checkout。
这会重新检出文件并替换合并冲突标记。 如果想要重置标记并尝试再次解决它们的话这会很有用。
可以传递给 –conflict 参数 diff3 或 merge(默认选项)。
如果传给它 diff3,Git 会使用一个略微不同版本的冲突标记: 不仅仅只给你 “ours” 和 “theirs” 版本,同时也会有 “base” 版本在中间来给你更多的上下文。$ git checkout --conflict=diff3 hello.rb$ git config --global merge.conflictstyle diff3
git checkout 命令也可以使用 --ours 和 --theirs 选项,这是一种无需合并的快速方式,你可以选择留下一边的修改而丢弃掉另一边修改。
合并日志
1 | $ git log --oneline --left-right HEAD...MERGE_HEAD |
组合式差异格式
这种叫作“组合式差异”的格式会在每一行给你两列数据。
如果我们解决冲突再次运行 git diff,我们将会看到同样的事情,但是它有一点帮助。
也可以在合并后通过 git log 来获取相同信息,查看冲突是如何解决的。
如果你对一个合并提交运行 git show 命令 Git 将会输出这种格式,
或者你也可以在 git log -p(默认情况下该命令只会展示还没有合并的补丁)命令之后加上 –cc 选项。$ git log --cc -p -1
撤消合并
修复引用
最简单且最好的解决方案是移动分支到你想要它指向的地方。 git reset --hard HEAD~,这会重置分支指向所以它们看起来像这样:
这个方法的缺点是它会重写历史,在一个共享的仓库中这会造成问题的。 查阅 变基的风险来了…
还原提交
其他类型的合并
到目前为止我们介绍的都是通过一个叫作 “recursive” 的合并策略来正常处理的两个分支的正常合并。
我们的或他们的偏好
如果你希望 Git 简单地选择特定的一边并忽略另外一边而不是让你手动解决冲突,你可以传递给 merge 命令一个 -Xours 或 -Xtheirs 参数。git merge -Xours mundo
这个选项也可以传递给我们之前看到的 git merge-file 命令, 通过运行类似 git merge-file –ours 的命令来合并单个文件。
如果想要做类似的事情但是甚至并不想让 Git 尝试合并另外一边的修改, 有一个更严格的选项,它是 “ours” 合并 策略。 $ git merge -s ours mundo
例如,假设你有一个分叉的 release 分支并且在上面做了一些你想要在未来某个时候合并回 master 的工作。
与此同时 master 分支上的某些 bugfix 需要向后移植回 release 分支。
你可以合并 bugfix 分支进入 release 分支同时也 merge -s ours 合并进入你的 master 分支 (即使那个修复已经在那儿了)这样当你之后再次合并 release 分支时,就不会有来自 bugfix 的冲突。
子树合并
子树合并的思想是你有两个项目,并且其中一个映射到另一个项目的一个子目录,
$_PS: 是否类似github fork 工作方式。
1 | $ git remote add rack_remote https://github.com/rack/rack |
这给我们提供了一种类似子模块工作流的工作方式,但是它并不需要用到子模块.
另外一个有点奇怪的地方是,当你想查看 rack 子目录和 rack_branch 分支的差异——你必须使用 git diff-tree 来和你的目标分支做比较:git diff-tree -p rack_branchgit diff-tree -p rack_remote/master
$_PS: 略。没用到
7.9 Rerere
正如它的名字“重用记录的解决方案(reuse recorded resolution)”所示,它允许你让 Git 记住解决一个块冲突的方法, 这样在下一次看到相同冲突时,Git 可以为你自动地解决它。
git config --global rerere.enabled true
有几种情形下这个功能会非常有用。
$_PS: 略。没有那么高级的需求,还是每次merge安全些(也没有很不方便)
所以,如果做了很多次重新合并,或者想要一个主题分支始终与你的 master 分支保持最新但却不想要一大堆合并, 或者经常变基,打开 rerere 功能可以帮助你的生活变得更美好。
7.10 使用 Git 调试
文件标注
它能显示任何文件中每行最后一次修改的提交记录。git blame -L 69,82 Makefile
这其中有一个很有意思的特性就是你可以让 Git 找出所有的代码移动。git blame -C -L 141,153 GITPackUpload.m
二分查找
1 | git bisect start |
事实上,如果你有一个脚本在项目是正常的情况下返回 0,在不正常的情况下返回非 0,你可以使 git bisect 自动化这些操作。1
2git bisect start HEAD v1.0
git bisect run test-error.sh
7.11 子模块
子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。1
2
3[submodule "DbConnector"]
path = DbConnector
url = https://github.com/chaconinc/DbConnector
$_PS: 看起来是会有一些使用难度。看情况使用(不是每个人都了)
开始使用子模块
1 | git submodule add https://github.com/chaconinc/DbConnector |
克隆含有子模块的项目
当你在克隆这样的项目时,默认会包含该子模块目录,但其中还没有任何文件git submodule init 用来初始化本地配置文件,
而 git submodule update 则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
如果给 git clone 命令传递 --recurse-submodules 选项,它就会自动初始化并更新仓库中的每一个子模块, 包括可能存在的嵌套子模块。
如果你已经克隆了项目但忘记了 –recurse-submodules,git submodule update --init --recursive。
在包含子模块的项目上工作
从子模块的远端拉取上游修改
可以进入到目录中运行 git fetch 与 git merge,合并上游分支来更新本地代码。如果你现在返回到主项目并运行
git diff --submodule,就会看到子模块被更新的同时获得了一个包含新添加提交的列表。git config --global diff.submodule log
git submodule update --remote DbConnector
此命令默认会假定你想要更新并检出子模块仓库的 master 分支。 不过你也可以设置为想要的其他分支。
既可以在 .gitmodules 文件中设置 (这样其他人也可以跟踪它),也可以只在本地的 .git/config 文件中设置git config -f .gitmodules submodule.DbConnector.branch stable
如果你设置了配置选项 status.submodulesummary,Git 也会显示你的子模块的更改摘要:git config status.submodulesummary 1
从项目远端拉取上游更改
默认情况下,git pull 命令会递归地抓取子模块的更改,如上面第一个命令的输出所示。 然而,它不会 更新 子模块。git submodule update --init --recursive
在子模块上工作
发布子模块改动
git push 命令接受可以设置为 “check” 或 “on-demand” 的 –recurse-submodules 参数。 如果任何提交的子模块改动没有推送那么 “check” 选项会直接使 push 操作失败。git push --recurse-submodules=checkgit push --recurse-submodules=on-demand
合并子模块改动
子模的块技巧
子模块遍历
有用的别名
子模块的问题
切换分支
从子目录切换到子模块
7.12 打包
bundle 命令会将 git push 命令所传输的所有内容打包成一个二进制文件1
2
3
4
5
6
7
8
9
10
11
12
13# 一个名为 repo.bundle 的文件,该文件包含了所有重建该仓库 master 分支所需的数据。
git bundle create repo.bundle HEAD master
git clone repo.bundle repo
## 如果你在打包时没有包含 HEAD 引用,你还需要在命令后指定一个 -b master 或者其他被引入的分支, 否则 Git 不知道应该检出哪一个分支。
git bundle create commits.bundle master ^9a466c5
# bundle verify 命令可以检查这个文件是否是一个合法的 Git 包,是否拥有共同的祖先来导入。
git bundle verify ../commits.bundle
# 查看这边包里可以导入哪些分支,同样有一个命令可以列出这些顶端:
git bundle list-heads ../commits.bundle
# 使用 fetch 或者 pull 命令从包中导入提交。 这里我们要从包中取出 master 分支到我们仓库中的 other-master 分支:
git fetch ../commits.bundle master:other-master
7.13 替换
replace 命令可以让你在 Git 中指定 某个对象 并告诉 Git:“每次遇到这个 Git 对象时,假装它是 其它对象”。
我们可以将新历史推送到新项目中,当其他人克隆这个仓库时,他们仅能看到最近两次提交以及一个包含上述说明的基础提交。
现在我们将以想获得整个历史的人的身份来初次克隆这个项目。
$_PS: 略。不懂意义何在/多大
7.14 凭证存储
Git 拥有一个凭证系统来处理这个事情。 下面有一些 Git 的选项:
git config --global credential.helper cache
“store” 模式可以接受一个 –file <path> 参数,可以自定义存放密码的文件路径(默认是 ~/.git-credentials )。
“cache” 模式有 –timeout <seconds> 参数,可以设置后台进程的存活时间(默认是 “900”,也就是 15 分钟)。
git config --global credential.helper 'store --file ~/.my-credentials'
Git 甚至允许你配置多个辅助工具。1
2
3[credential]
helper = store --file /mnt/thumbdrive/.git-credentials
helper = cache --timeout 30000
底层实现
自定义凭证缓存
$_PS: 略。只需知道,上面的凭证存储的3+2种模式就好